feat!: remove users.display_name, resolve identity from handle - #5
Merged
Conversation
The participant list carried a caller-supplied display_name, which every client populated with the user's own email address. A stranger admitted by knock therefore learned every participant's real email. The name is now read from users.handle by the authenticated user id, so it cannot be set over the wire — which also removes the impersonation vector sanitize_display_name and MAX_DISPLAY_NAME_CHARS existed to defend, and both are deleted with it. display_name stays in the emitted JSON as an alias carrying the handle, so pre-0.26 clients keep rendering a name. Delete in 0.27.
Two comments near invited_by_handle still described display_name as caller-supplied — no longer true after resolving it server-side from users.handle. Rewritten to state what the code does now. Also found while re-running the suite: the new handle-resolution test used a fixed literal handle, which collides with itself on a second run against a persistent test database (users.handle is unique and never recycled). Switched to test_support::unique_handle, matching the rest of the suite.
The prior wording claimed the participant list "isn't available yet" — wrong; it's populated but deliberately redacted to empty for an unaccepted stranger in list_active_sessions. Point at that instead of restating it.
Every query that selected display_name selects handle. The display_name JSON key survives as an alias carrying the handle for pre-0.26 clients; delete in 0.27. A pending invitation to an address with no account still shows the email, which is what keeps a handle-only roster mappable to a person.
Teammate fuzzy matching survives on email, which is the half that matters — a teammate's address is already known to a teammate. Custom-handle fuzzy and the two exact branches are unchanged.
There is nothing left to set. /v1/auth/me reports the handle, under the display_name alias as well, for pre-0.26 clients.
The column is gone. Every display_name that survives in the API is an alias carrying the handle, for clients that have not yet updated; all of them are deleted in 0.27.
…field 036 drops the column with no down-migration and no room for a rolling restart across it; record that on the migration itself. UserSearchResult's display_name was the one alias that landed (Task 3) without the comment the others use, so it would have survived the 0.27 grep undetected.
…_name DROP COLUMN IF EXISTS makes a manual replay of 036 safe; production has never run it. PendingInvitation.display_name is the one server field that is not a pre-0.26 alias — mark it as the deliberate exception so a 0.27 sweep does not read its silence as a missed deletion.
This was referenced Aug 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Removes
users.display_nameentirely.users.handlebecomes the sole human-facing identifier.Why
The participant list broadcast every participant's email address. All four client call sites passed
getCurrentUserEmail()as the WebSocketdisplay_namequery parameter, and the server echoed it to every participant — so a stranger admitted by a knock invite learned everyone's real email. The name is now resolved fromusers.handleby the authenticated user id, so it cannot be set over the wire.That also removes the impersonation vector ("Voltius Support") that
sanitize_display_name,MAX_DISPLAY_NAME_CHARSand the control-character refusal existed to defend. All three are deleted rather than hardened.Unplanned bonus:
get_invitationis unauthenticated and used to hand anyone holding an invite token the inviter's email local part. It now returns the handle.The alias window
For this release the server still emits
display_nameJSON keys whose value is the handle, so clients that have not yet updated render a handle rather than blank rows. Every one carries a comment. They all come out in 0.27 —display_name(×5 structs),invited_by_display_name,inviter_display_name, the plugin API'sdisplayName, theWsQuery.display_namesink, and theACCOUNT_CACHE_KEYSpurge entry.PendingInvitation.display_nameis not an alias — it is the deliberate exception, populated byCOALESCE(invitee.handle, pi.email), because an invitee with no account has no handle. That is what keeps a handle-only roster mappable back to a person.registerand/v1/auth/mefail the moment the column is gone.Testing
240 tests,
cargo clippy --all-targets -- -D warningsclean. Migration 036 applied to a fresh database and verified:display_nameabsent,handle+handle_is_custompresent.